001.Mem0 源码实现与原理解析
Mem 0 是什么,解决什么问题
在工程语境里,Mem 0 可以理解为一层面向 LLM 应用的 长期记忆基础设施。它不直接替代大模型,也不直接替代应用层 agent,而是位于两者之间,专门负责把对话中的“值得长期保留的信息”提炼、存储、检索,并在后续请求中重新提供给上层系统。
它想解决的核心问题不是“怎么让模型看见更多上下文”,而是“怎么让系统只记住真正重要、可持续复用的信息”。如果没有这层 memory pipeline,常见方案通常会落入两个极端:
- 一种是把越来越长的对话原文直接塞进 prompt,成本高、延迟大,而且上下文越长越噪
- 一种是把对话粗暴切块后做 RAG,能检索到相似文本,但未必能维护稳定的用户画像、偏好和状态
Mem 0 的工程理念是:不要把长期记忆等同于原始上下文,而是把对话转成可维护的 memory state。 这也是为什么它的源码主线不是 chat,而是 extraction -> update -> storage -> retrieval。
1. 项目整体架构概览
1.1 Mem 0 在 LLM 系统中的位置
从代码视角看,Mem 0 不是一个“大模型应用”,而是一层插在应用逻辑与底层存储之间的 memory orchestration layer。它位于:
应用/Agent -> Mem0 SDK -> LLM/Embedder/Vector Store/(可选) Graph Store -> 应用组装 Prompt
在仓库里,这个位置体现得非常明确:
- 对外入口在
mem0/__init__.py,导出Memory、AsyncMemory、MemoryClient、AsyncMemoryClient - Memo 0 核心逻辑实现在
mem0/memory/main.py - 托管平台客户端在
mem0/client/main.py - 真正把记忆注入 prompt 的包装器不在
Memory.search(),而在mem0/proxy/main.py
这说明 Mem 0 的核心职责不是替你完成对话,而是负责两件事:
- 把对话写成可检索、可更新的 memory
- 在查询时把相关 memory 取回来,供上层应用决定如何使用
1.2 与传统 RAG / memory buffer 的区别
从实现上看,Mem 0 与传统 RAG、memory buffer 的关键差异不在“检索”本身,而在 写入时就先做记忆压缩与结构化。
和传统 RAG 相比:
- RAG 通常把文档 chunk 后直接 embedding,再做相似度检索
- Mem 0 在
Memory.add()里先调用 LLM 做fact extraction,把对话转成更短、更稳定的事实,再写入向量库
和 memory buffer 相比:
- buffer 主要保留原始对话序列,依赖上下文窗口回放
- Mem 0 只保留“值得记住的事实”,并支持
ADD / UPDATE / DELETE / NONE
为什么这样设计:
- 对话原文噪声大、冗余高,直接存全文会放大检索误差
- 真正要长期保留的是稳定事实,而不是每一轮措辞
- 让 LLM 在写入阶段先做归纳,可以把“prompt token 消耗”前移为“memory maintenance”
1.3 高层组件关系图
可以把 Mem 0 的高层结构理解为以下文字关系图:
SDK/API 调用
-> Memory / AsyncMemory
-> factory 组装依赖
-> LLM 做 memory extraction 与 update decision
-> Embedder 生成向量
-> VectorStore 持久化与检索
-> SQLiteManager 记录变更历史
-> (可选) MemoryGraph 维护实体关系图
-> 上层应用或 proxy 将结果注入 prompt
这里最重要的工程思想是:编排层、模型层、存储层、注入层是解耦的。Memory 负责 orchestration,provider 细节交给 factory 和适配器。

2. 仓库目录结构解析
2.1 顶层目录职责
按照工程实现的重要性,可以把仓库顶层目录分成三层。
第一层,核心实现:
mem0/:Python 主 SDK,源码分析主要入口mem0-ts/:TypeScript/Node SDK,和 Python 并列的正式实现server/:基于Memory的 FastAPI 封装,展示“SDK 如何服务化”
第二层,产品化与生态集成:
openmemory/:基于 Mem 0 的完整应用栈,属于上层产品,而不是底层记忆引擎vercel-ai-sdk/:面向 Vercel AI SDK 的集成层openclaw/:外部 agent 宿主的插件式集成
第三层,外围支撑材料:
examples/:示例代码,不是核心逻辑cookbooks/:教程、Notebook、实践示例docs/:文档站内容tests/:测试evaluation/:评测与实验代码embedchain/:同仓库下的相邻项目,不是mem0/内核的一部分
2.2 mem0/ 包内部结构
mem0/ 目录内部可以继续拆成几个关键子系统:
mem0/memory/:核心编排层,Memory、AsyncMemory、SQLiteManager,以及默认图记忆实现都在这里mem0/client/:托管平台客户端,负责 HTTP API 调用mem0/vector_stores/:向量库抽象与各 provider 适配器mem0/embeddings/:embedding provider 适配器mem0/llms/:LLM provider 适配器mem0/graphs/:图相关工具、配置,以及 Neptune 等图后端实现mem0/reranker/:检索重排层mem0/configs/:Pydantic 配置模型、prompt 模板、数据结构定义mem0/utils/:工厂与通用工具,尤其是factory.pymem0/proxy/:聊天包装层,负责把 memory 回填到 prompt
3. 系统启动与主调用链
3.1 Python SDK 的启动入口
对 Python 用户来说,入口非常简单:
from mem0 import Memory- 实际导出位置是
mem0/__init__.py - 真正实例化的类在
mem0/memory/main.py的Memory
Memory.__init__() 会完成依赖装配:
EmbedderFactory.create(...)VectorStoreFactory.create(...)LlmFactory.create(...)SQLiteManager(...)(可选) RerankerFactory.create(...)(可选) GraphStoreFactory.create(...)
这一步决定了 Mem 0 的核心设计:Memory 自己不实现模型和存储,而是做统一编排器。这样才能支持 OpenAI、Ollama、Qdrant、PGVector、Neo 4 j 等多种 provider。
3.2 API / Server 调用入口
如果从 REST API 进入,则调用链是:
server/main.py:add_memory()
-> MEMORY_INSTANCE.add(...)
-> Memory.add(...)
检索同理:
server/main.py:search_memories()
-> MEMORY_INSTANCE.search(...)
-> Memory.search(...)
因此 server/ 只是 transport layer,不承载核心逻辑。真正的业务都在 Memory 内部。
3.3 写入主链路:add()
一次标准写入的函数级调用链可以概括为:
Memory.add()
-> _build_filters_and_metadata()
-> 输入归一化(str/dict/list)
-> parse_vision_messages()
-> 并发执行 _add_to_vector_store() 与 _add_to_graph()
其中 _add_to_vector_store() 又分成两条路径。
第一条,原文直存路径:
infer=False
-> 对每条 message 直接 embedding_model.embed(...)
-> _create_memory()
-> vector_store.insert(...)
-> SQLiteManager.add_history(..., "ADD")
第二条,默认的“推断式 memory”路径:
infer=True
-> parse_messages()
-> _should_use_agent_memory_extraction()
-> get_fact_retrieval_messages()
-> llm.generate_response(...json...)
-> remove_code_blocks() / extract_json()
-> 得到 facts
-> 对每个 fact 做相似记忆召回
-> get_update_memory_messages(...)
-> 第二次 llm.generate_response(...json...)
-> 执行 ADD / UPDATE / DELETE / NONE
为什么要拆成两段 LLM:
- 第一段负责“从对话抽事实”
- 第二段负责“让新事实与旧记忆对齐”
这比分别靠规则做 extraction 和 dedup 更稳,因为真实对话里常见同义改写、信息补全、轻微冲突和时态变化。

3.4 检索主链路:search()
检索路径更短,也更像传统检索系统:
Memory.search()
-> _build_filters_and_metadata()
-> 处理 advanced metadata filters
-> 并发执行 _search_vector_store() 与 graph.search()
-> (可选) reranker.rerank(...)
-> 返回 {"results": ..., "relations": ...}
向量检索内部链路是:
Memory._search_vector_store()
-> embedding_model.embed(query, "search")
-> vector_store.search(...)
-> 组装 MemoryItem
-> 应用 threshold
这说明 Mem 0 的检索阶段刻意保持轻量:尽量把复杂性放在写入时,检索时只做 embedding + ANN + optional rerank。

3.5 读取单条 memory:get()
Memory.get() 的路径非常直接:
Memory.get(memory_id)
-> vector_store.get(vector_id=memory_id)
-> payload 转成 MemoryItem
它不做 LLM 推理,也不查图数据库。原因很简单:按 ID 获取是存储层操作,不应该再经过语义层。
4. Mem 0 的核心数据流
4.1 一次对话输入后的阶段划分
从数据流角度,一次对话写入可以拆成六个阶段。
第一阶段,输入归一化与作用域绑定:
- 把
str、dict、list[dict]统一成 message list - 通过
_build_filters_and_metadata()绑定user_id、agent_id、run_id
为什么先做这一步:
- Mem 0 不是全局共享记忆池,而是有明确 session scope
- 所有后续检索、更新、图谱写入都依赖这组 scope
第二阶段,memory extraction:
parse_messages()把对话展平为文本get_fact_retrieval_messages()选择用户记忆或 agent 记忆 promptllm.generate_response()输出{"facts": [...]}
为什么使用 fact extraction:
- 对话是过程数据,memory 是状态数据
- extraction 的目标是把过程压缩为状态
第三阶段,embedding / storage 准备:
- 对每个新 fact 调
embedding_model.embed(fact, "add") - 用这个 embedding 去
vector_store.search(...)找到最相近的已有记忆
这里有一个非常重要的设计点:新事实在真正入库前,会先走一次“用自己检索自己”的流程。这不是多余,而是 update / dedup 的前置步骤。
第四阶段,retrieval / ranking for update:
- 每个 fact 召回 top-k 旧记忆
- 合并去重成
retrieved_old_memory - 临时把真实 UUID 映射成
0/1/2/... - 构造 update prompt,让 LLM 输出 action plan
为什么不用规则去重:
- “喜欢奶酪披萨” 与 “最喜欢和朋友一起吃奶酪披萨” 是扩展还是重复,规则很难判
- Mem 0 选择“向量召回候选 + LLM 决策”的混合策略
第五阶段,持久化:
ADD->_create_memory()UPDATE->_update_memory()DELETE->_delete_memory()NONE-> 只在必要时补充agent_id/run_id
写入的真实落点有两个:
- 向量库保存当前 memory state
SQLiteManager保存 memory history
为什么双存储:
- 向量库负责在线检索
- SQLite 历史库负责审计、追踪和回放 memory 演化过程
第六阶段,检索后如何注入 prompt:
严格来说,Memory.search() 只返回结构化结果,不负责 prompt 注入。仓库里内置的自动回填实现位于 mem0/proxy/main.py:
Completions.create()
-> _fetch_relevant_memories()
-> _format_query_with_memories()
-> litellm.completion(...)
为什么把 prompt 注入放在 proxy 层而不是 Memory.search():
Memory应保持为基础设施组件,只负责“取回记忆”- “如何拼 prompt” 属于应用策略,不应固化在存储检索层
5. 关键模块源码解析
5.1 Memory manager / store
这是整个系统的控制中心,核心类是:
mem0/memory/main.py:Memorymem0/memory/main.py:AsyncMemorymem0/memory/storage.py:SQLiteManagermem0/vector_stores/base.py:VectorStoreBase
输入输出结构:
- 输入:
messages、query、user_id/agent_id/run_id、metadata - 输出:统一为
{"results": [...]},搜索时可附带{"relations": [...]} - 单条 memory 的标准结构由
mem0/configs/base.py中的MemoryItem定义
关键职责:
Memory:主编排器VectorStoreBase:统一向量存储接口- 具体 store,例如
Qdrant:把抽象接口落到 provider SDK SQLiteManager:维护 history 表
核心策略:
- 当前 memory 的“真值”放在向量库 payload 中
hash、created_at、updated_at与业务 metadata 一起存入 payload- 所有修改都写入 history,而不是只保留最终状态
为什么这样设计:
- 如果只存最终状态,就无法解释 memory 为何被改写
- 如果只存 history,在线检索代价又太高
- 所以 Mem 0 采用
current state + append-only history的组合
5.2 Memory extraction pipeline
这个模块的关键函数分布在:
mem0/memory/main.py:_add_to_vector_storemem0/memory/utils.py:parse_messagesmem0/memory/utils.py:get_fact_retrieval_messagesmem0/configs/prompts.py
输入输出结构:
- 输入:message list
- 输出:
facts: list[str]
关键逻辑:
parse_messages()把多轮消息转成"user: ...\n assistant: ..."形式_should_use_agent_memory_extraction()判断当前是提取用户事实还是 agent 事实USER_MEMORY_EXTRACTION_PROMPT与AGENT_MEMORY_EXTRACTION_PROMPT通过 few-shot 约束输出 JSONremove_code_blocks()与extract_json()负责做 LLM 输出清洗
核心算法或策略:
- 不是对每条消息直接入库,而是先做语义抽取
- prompt 明确要求“只从 user 消息”或“只从 assistant 消息”提事实
- 输出强制 JSON 化,减少下游解析脆弱性
为什么这样设计:
- 真实对话里 assistant 会复述用户信息,如果不区分角色,memory 容易被污染
- 先抽事实再入库,比直接存对话文本更稳定,也更利于跨轮召回
5.3 Retrieval & scoring 逻辑
向量检索相关的关键位置是:
mem0/memory/main.py:_search_vector_storemem0/vector_stores/base.py- 某个具体 store,例如
mem0/vector_stores/qdrant.py (可选) mem0/reranker/*
输入输出结构:
- 输入:
query、scope filters、limit、threshold - 输出:带
score的MemoryItem列表
关键流程:
embedding_model.embed(query, "search")vector_store.search(query, vectors, limit, filters)- 将底层命中结果转成统一的
MemoryItem - 如果配置了 reranker,再执行
reranker.rerank(query, original_memories, limit)
评分逻辑的本质:
- 第一层分数来自底层向量库相似度,例如 Qdrant 的 cosine score
- 第二层分数可由 reranker 重排覆盖
- graph search 不参与改写向量结果排序,只附加
relations
图检索路径则在 mem0/memory/graph_memory.py:search():
- 先从 query 中抽实体
- 在图库里找相近节点及其关系
- 用
BM25Okapi对[source, relationship, destination]三元组做 rerank
为什么这样设计:
- 向量检索适合“找相关记忆”
- 图检索适合“补充关系上下文”
- 把二者并行执行,而不是强行混成一个统一分数,工程上更稳,也更容易替换后端
5.4 Memory 更新与去重策略
这是 Mem 0 最有代表性的部分,也是分享时最值得展开的地方。
关键函数:
mem0/memory/main.py:_add_to_vector_storemem0/configs/prompts.py:get_update_memory_messagesmem0/memory/main.py:_create_memorymem0/memory/main.py:_update_memorymem0/memory/main.py:_delete_memory
输入输出结构:
- 输入:
new_retrieved_facts和召回到的retrieved_old_memory - 输出:
{"memory": [{"id", "text", "event", "old_memory"}]}
核心策略不是简单的字符串去重,而是四段式管线:
- 抽取新事实
- 用每个新事实去向量库召回相似旧记忆
- 对候选旧记忆去重并做 UUID 临时映射
- 让 LLM 决定
ADD / UPDATE / DELETE / NONE
其中有几个很有代表性的工程细节:
unique_data:按真实 memory ID 去重,避免多条新事实重复召回同一旧记忆temp_uuid_mapping:把 UUID 映射成短 ID,降低 LLM 幻觉 UUID 的概率hash:只是 payload 元数据,不是主去重依据NONE:不是纯 no-op,必要时仍会补齐agent_id/run_id
为什么这样设计:
- memory 的变化常常不是“新旧字符串是否相等”,而是“语义是否被覆盖、扩展、冲突”
- 纯 hash 只能做字面级重复检测,无法处理事实演化
- 纯向量阈值又无法明确判断“该新增还是该覆盖”
- 所以 Mem 0 采用
semantic retrieval + LLM action planning的混合决策模式
这也是 Mem 0 相比“向量库加几条规则”的本质升级点。
5.5 Graph memory 作为扩展模块
图增强路径的默认关键类是 mem0/memory/graph_memory.py:MemoryGraph。这条路径使用 Neo4jGraph 作为默认图访问接口;如果切换到 Neptune 等后端,则对应实现会落在 mem0/graphs/。
输入输出结构:
- 写入输入:整段文本和 session filters
- 写入输出:
{"deleted_entities": ..., "added_entities": ...} - 检索输出:
[{"source", "relationship", "destination"}]
关键流程:
写入:
MemoryGraph.add()
-> _retrieve_nodes_from_data()
-> _establish_nodes_relations_from_data()
-> _search_graph_db()
-> _get_delete_entities_from_search_output()
-> _delete_entities()
-> _add_entities()
检索:
MemoryGraph.search()
-> _retrieve_nodes_from_data(query, filters)
-> _search_graph_db(...)
-> BM25Okapi 重排关系三元组
核心算法或策略:
- LLM tool calling 做实体与关系抽取
- 节点也带 embedding,用于图内“近似同一实体”匹配
- 图写入时会先搜索相似节点,避免为同一实体反复造新节点
为什么这样设计:
- 图记忆的价值不在“再存一份文本”,而在显式表示关系
- 关系更新和节点合并都很难靠规则完成,所以图路径同样依赖 LLM + embedding 双重判断
6. Mem 0 与 Mem 0^g 的区别
从原论文中的命名上看,Mem 0^g 是图增强版;从开源实现上来看,它并不是一个完全独立的新系统,而是 同一个 Memory 主干加上 graph_store 配置后的运行模式。
在代码层面的差异主要有三点。
第一,写入路径不同:
- Mem 0:只走
_add_to_vector_store() - Mem 0^g:
Memory.add()中并发执行_add_to_vector_store()和_add_to_graph()
第二,检索结果结构不同:
- Mem 0:返回
results - Mem 0^g:返回
results + relations
第三,推理目标不同:
- Mem 0 偏向单条事实召回
- Mem 0^g 额外保留实体关系,适合多跳、时序、结构化问题
但必须强调一个源码事实:
- 图结果不会自动重排向量结果
Memory.search()里的排序主导权仍在向量检索和可选 reranker- 图层更像 relation side-channel,而不是替代 vector retriever
为什么这样做:
- 向量检索仍然是更稳、更通用的主路径
- 图检索适合增强,不适合替代主召回
- 让 graph 作为 optional capability,能把复杂性和延迟控制在可接受范围
7. 设计权衡与工程决策分析
7.1 为什么选择当前架构
Mem 0 当前架构的核心思想可以概括为一句话:
把“理解对话、整理事实、维护记忆”前置到写入时完成,把检索时的在线开销压缩到最小。
这体现在:
- 写入时用了两次 LLM:一次抽事实,一次决策更新
- 检索时通常只做 embedding + ANN + optional rerank
这样设计的好处是:
- 在线延迟更可控
- prompt 更短
- memory 更稳定,不会被对话原文噪声拖累
代价也很明确:
- 写入比普通 vector append 更重
- memory 质量受 extraction prompt 和 LLM 稳定性影响
- update decision 仍然存在概率性误判
7.2 与传统 RAG / vector memory 的 trade-off
相对于传统 RAG:
- 优势:不是检索 chunk,而是检索“归纳后的事实”,更适合长期个性化记忆
- 劣势:不适合做高保真原文证据回溯
相对于纯 vector memory:
- 优势:支持 memory 生命周期管理,不只是 append-only
- 劣势:系统更复杂,写路径依赖 LLM
相对于 conversation buffer:
- 优势:不会无限膨胀上下文
- 劣势:会丢失部分对话细节,尤其是未被抽取成 fact 的信息
7.3 哪些地方体现了论文思想,哪些是工程妥协
体现论文思想的地方:
- 写入前 extraction,而不是原文直存
- memory 以最小事实单元存在,而不是长上下文回放
- 图增强路径用于补充关系型推理
明显的工程妥协也很清楚:
- history 存在本地
SQLiteManager,而不是统一远程事务系统 - graph search 结果作为并行补充返回,没有和向量分数做统一融合
- provider 通过 factory 动态装配,优先解决“可插拔”,而不是极致一致性
proxy/main.py里 prompt injection 逻辑比较轻量,说明它只是一个 封装层,而不是完整 agent runtime
这些妥协背后的原因并不负面,恰恰说明 Mem 0 的目标是 可部署、可替换、可扩展,而不是追求理论上最优的单体架构。
8. 一次完整请求的端到端流程复盘
场景:应用收到一轮新对话,想把它写入 memory,并在下一轮回答前检索相关记忆。

8.1 写入阶段
- 应用调用
Memory.add(messages, user_id=...),或者通过server/main.py的/memoriesAPI 间接调用。 Memory.add()先用_build_filters_and_metadata()生成作用域 metadata,保证后续操作都限定在当前用户/agent/run 下。- 输入消息被归一化;如果包含图像,
parse_vision_messages()先把图像转成文字描述。 Memory.add()并发启动两条支路:向量记忆支路_add_to_vector_store(),以及可选图记忆支路_add_to_graph()。- 在向量支路里,
parse_messages()把对话展平;get_fact_retrieval_messages()选择 prompt;llm.generate_response()输出facts。 - 每个 fact 会先被 embedding,然后去
vector_store.search()中召回 top-k 旧记忆候选。 - 系统把这些候选去重后交给第二个 LLM prompt,让模型输出 action plan:每条新事实应该
ADD、UPDATE、DELETE还是NONE。 Memory根据 action plan 调用_create_memory()、_update_memory()或_delete_memory(),把当前状态写入向量库,同时通过SQLiteManager.add_history()记录变更历史。- 如果启用了图记忆,
MemoryGraph.add()同步完成实体提取、关系建立、冲突删除与节点/边合并。
8.2 检索阶段
- 当应用要回答用户新问题时,调用
Memory.search(query, user_id=...)。 Memory.search()生成 query embedding,并通过_search_vector_store()从向量库召回最相关的 memory。- 如果配置了 reranker,调用
reranker.rerank(...)做二次排序。 - 如果启用了图记忆,
graph.search()并发返回实体关系三元组,作为relations附加结果。
8.3 Prompt 注入阶段
- 如果应用自己掌控 prompt,它可以直接把
search()["results"]拼进 system prompt;如果使用proxy,这一步则由包装层自动完成。 - 如果使用
mem0/proxy/main.py的Completions.create(),则会自动:- 后台启动
_async_add_to_memory() - 同步执行
_fetch_relevant_memories() - 通过
_format_query_with_memories()把 memory 拼到用户问题前 - 最后调用
litellm.completion(...)

- 后台启动
它强调的是职责边界:Memory 负责提供 memory,应用层或 proxy 负责决定如何消费 memory。